﻿namespace ScreenShoter
{
	using System;
	using System.Collections.Generic;
	using System.ComponentModel;
	using System.Runtime.InteropServices;
	using System.Text;
	using System.Security.Permissions;
	using System.Globalization;

	/// <summary>
	/// Represents a global low level keyboard hook.
	/// </summary>
	public class KeyboardHook : IDisposable
	{
		static Dictionary<EnhancedKeys, string> currentHookKeys = new Dictionary<EnhancedKeys, string>();
		IntPtr hookID;
		bool isDisposed, wasHookKeyReleased = true;

		/// <summary>
		/// Initializes a new instance of the KeyboardHook class.
		/// </summary>
		public KeyboardHook() { }

		/// <summary>
		/// Initializes a new instance of the KeyboardHook class.
		/// </summary>
		/// <param name="actionDescription">A value indicating the functional description for the KeyboardHook.</param>
		public KeyboardHook(string actionDescription)
		{
			this.ActionDescription = actionDescription;
		}

		/// <summary>
		/// Finalizes an instance of the KeyboardHook class.
		/// </summary>
		~KeyboardHook()
		{
			this.Dispose(false);
		}

		/// <summary>
		/// Occurs when the KeyboardHook's assigned key combination is pressed.
		/// </summary>
		public event EventHandler<EventArgs> Pressed;

		/// <summary>
		/// Gets or sets a value indicating the functional description for the KeyboardHook.
		/// </summary>
		public string ActionDescription { get; set; }

		/// <summary>
		/// Gets the key combination currently assigned to the KeyboardHook.
		/// </summary>
		public EnhancedKeys Keys { get; private set; }

		/// <summary>
		/// Gets the unmodified key currently assigned to the KeyboardHook.
		/// </summary>
		public EnhancedKeys UnmodifiedKey
		{
			get { return this.Keys & EnhancedKeys.KeyCode; }
		}

		/// <summary>
		/// Gets or sets a value indicating whether to allow normal processing of key strokes in 
		/// addition to the KeyboadHook's Pressed event.
		/// </summary>
		public bool AllowPassThrough { get; set; }

		/// <summary>
		/// Gets a value indicating whether the Control modifier key is currently assigned to the KeyboardHook.
		/// </summary>
		public bool Control
		{
			get { return (this.Keys & EnhancedKeys.Control) == EnhancedKeys.Control; }
		}

		/// <summary>
		/// Gets a value indicating whether the Shift modifier key is currently assigned to the KeyboardHook.
		/// </summary>
		public bool Shift
		{
			get { return (this.Keys & EnhancedKeys.Shift) == EnhancedKeys.Shift; }
		}

		/// <summary>
		/// Gets a value indicating whether the Alt modifier key is currently assigned to the KeyboardHook.
		/// </summary>
		public bool Alt
		{
			get { return (this.Keys & EnhancedKeys.Alt) == EnhancedKeys.Alt; }
		}

		/// <summary>
		/// Gets a value indicating whether the Windows Logo modifier key is currently assigned to the KeyboardHook.
		/// </summary>
		public bool Windows
		{
			get { return (this.Keys & EnhancedKeys.WinLogo) == EnhancedKeys.WinLogo; }
		}

		/// <summary>
		/// Gets or sets a value indicating whether the KeyboardHook Pressed event is fired repeatedly for the duration
		/// of the key press or only once per key press.
		/// </summary>
		public bool AutoRepeat { get; set; }

		/// <summary>
		/// Gets a value indicating whether the KeyboardHook is currently active.
		/// </summary>
		public bool IsEngaged
		{
			get { return this.hookID != IntPtr.Zero; }
		}

		/// <summary>
		/// Gets a value indicating whether the KeyboardHook currently has keys assigned to it.
		/// </summary>
		public bool IsEmpty
		{
			get { return this.Keys == EnhancedKeys.None; }
		}

		/// <summary>
		/// Attempts to set the key combination for the KeyboardHook. If the process fails, a return parameter indicates the reason
		/// for the failure.
		/// </summary>
		/// <param name="keys">The keys to set to the KeyboardHook.</param>
		/// <param name="errorMessage">If the method fails, returns an error message explaining the failure. Otherwise,
		/// returns an empty string.</param>
		/// <returns>True if the method succeeds; otherwise false.</returns>
		public bool TrySetKeys(EnhancedKeys keys, out string errorMessage)
		{
			errorMessage = string.Empty;
			if (this.Keys == keys)
			{
				return true;
			}

			if (this.IsEngaged)
			{
				errorMessage = "Cannot set keys while KeyboardHook is engaged.";
				return false;
			}

			if ((keys & EnhancedKeys.Modifiers) == EnhancedKeys.Modifiers)
			{
				errorMessage = "Keys cannot consist only of modifier keys.";
				return false;
			}

			string actionDescription;
			if (KeyboardHook.currentHookKeys.TryGetValue(keys, out actionDescription))
			{
				errorMessage = String.Format(CultureInfo.CurrentCulture,
					"The key combination ''{0}'' is already in use for the following action: {1}", 
					keys.ToString(), 
					actionDescription);

				return false;
			}

			this.Keys = keys;

			// If this hook already had keys, remove them from the dictionary before adding the new keys/description.
			if (!this.IsEmpty)
			{
				KeyboardHook.currentHookKeys.Remove(keys);
			}

			KeyboardHook.currentHookKeys.Add(keys, this.ActionDescription);
			return true;
		}

		/// <summary>
		/// Installs the KeyboardHook on the current system.
		/// </summary>
		/// <exception cref="InvalidOperationException">Occurs if the KeyboardHook is empty OR if the KeyboardHook is already engaged.</exception>
		/// <exception cref="ObjectDisposedException">Occurs if the KeyboardHook has been disposed.</exception>
		[SecurityPermission(SecurityAction.LinkDemand, Flags = SecurityPermissionFlag.UnmanagedCode)]
		public void Engage()
		{
			if (this.isDisposed)
			{
				throw new ObjectDisposedException("KeyboardHook");
			}

			if (this.IsEmpty)
			{
				throw new InvalidOperationException("Cannot engage KeyboardHook when keys are empty.");
			}

			if (this.IsEngaged)
			{
				throw new InvalidOperationException("KeyboardHook is already engaged.");
			}

			this.hookID = NativeMethods.SetWindowsHookEx(
				NativeMethods.Constants.WH_KEYBOARD_LL,
				this.HookCallBack,
				IntPtr.Zero,
				0);

			if (this.hookID == IntPtr.Zero)
			{
				throw new Win32Exception(Marshal.GetLastWin32Error());
			}
		}

		/// <summary>
		/// Removes the KeyboardHook from the system.
		/// </summary>
		/// <remarks>Disengage removes the hook from the system, but maintains all its data. Use Engage to re-install the hook. To
		/// discard the hook and all its data, use Dispose. It is not necessary to call Disengage prior to Dispose.</remarks>
		public void Disengage()
		{
			if (this.hookID != IntPtr.Zero)
			{
				NativeMethods.UnhookWindowsHookEx(this.hookID);
				this.hookID = IntPtr.Zero;
			}
		}

		/// <summary>
		/// Returns the string representation of the KeyboardHook.
		/// </summary>
		/// <returns>A string representing the KeyboardHook.</returns>
		public override string ToString()
		{
			if (this.IsEmpty)
			{
				return string.Empty;
			}

			StringBuilder output = new StringBuilder(50);
			output.Append(this.Windows ? "WinLogo + " : string.Empty);
			output.Append(this.Control ? "Ctrl + " : string.Empty);
			output.Append(this.Shift ? "Shift + " : string.Empty);
			output.Append(this.Alt ? "Alt + " : string.Empty);
			output.Append(this.UnmodifiedKey.ToString());

			return output.ToString();
		}

		/// <summary>
		/// Disengages the KeyboardHook and releases all associated resources.
		/// </summary>
		public void Dispose()
		{
			this.Dispose(true);
			GC.SuppressFinalize(this);
		}

		/// <summary>
		/// Disengages the KeyboardHook, releases all unmanaged resources, and optionally releases
		/// all managed resources.
		/// </summary>
		/// <param name="isDisposing">True to release both managed and unmanaged resources; false to 
		/// release only unmanaged resources.</param>
		protected virtual void Dispose(bool isDisposing)
		{
			if (!this.isDisposed)
			{
				if (isDisposing)
				{
					// No managed resources to dispose.
				}

				this.Disengage();
				KeyboardHook.currentHookKeys.Remove(this.Keys);
				this.isDisposed = true;
			}
		}

		/// <summary>
		/// Raises the Pressed event.
		/// </summary>
		/// <param name="sender">The source of the hook event.</param>
		/// <param name="e">An EventArgs that contains the event data.</param>
		/// <remarks>The Pressed event is executed asynchronously.</remarks>
		protected virtual void OnPressed(object sender, EventArgs e)
		{
			if (this.Pressed != null)
			{
				// Invoke these asychronously in case they're slow. (Testing revealed that Windows will reassert  
				// responsibility for the key stroke(s) if the hookcallback doesn't immediately return.)
				Delegate[] pressedEvents = Pressed.GetInvocationList();
				foreach (EventHandler<EventArgs> pressedEvent in pressedEvents)
				{
					pressedEvent.BeginInvoke(this, e, ar => ((EventHandler<EventArgs>)ar.AsyncState).EndInvoke(ar), pressedEvent);
				}
			}
		}

		/// <summary>
		/// Returns a value indicating whether the KeyboardHook's modifier keys are currently pressed.
		/// </summary>
		/// <returns>True of all modifier keys are currently pressed; otherwise, false.</returns>
		protected bool AreModifierKeysPressed()
		{
			if (this.Control)
			{
				if (!KeyboardHook.IsKeyPressed(EnhancedKeys.ControlKey))
				{
					return false;
				}
			}

			if (this.Shift)
			{
				if (!KeyboardHook.IsKeyPressed(EnhancedKeys.ShiftKey))
				{
					return false;
				}
			}

			if (this.Alt)
			{
				if (!(KeyboardHook.IsKeyPressed(EnhancedKeys.LMenu) || KeyboardHook.IsKeyPressed(EnhancedKeys.RMenu)))
				{
					return false;
				}
			}

			if (this.Windows)
			{
				if (!(KeyboardHook.IsKeyPressed(EnhancedKeys.LWin) || KeyboardHook.IsKeyPressed(EnhancedKeys.RWin)))
				{
					return false;
				}
			}

			return true;
		}

		/// <summary>
		/// Returns a value indicating whether a key is currently pressed.
		/// </summary>
		/// <param name="key">The key to check. Key must not be a modifier bit mask.</param>
		/// <returns>True if the key is currently pressed; otherwise false.</returns>
		/// <exception cref="ArgumentException">Occurs if key is a modifier bit mask. </exception>
		private static bool IsKeyPressed(EnhancedKeys key)
		{
			if ((key & EnhancedKeys.Modifiers) == EnhancedKeys.Modifiers)
			{
				throw new ArgumentException("Key cannot contain any modifiers.", "key");
			}

			return (NativeMethods.GetKeyState((int)key) & NativeMethods.Constants.KEY_PRESSED) == NativeMethods.Constants.KEY_PRESSED;
		}

		/// <summary>
		/// The callback proceedure for the installed KeyboardHook. 
		/// </summary>
		/// <param name="nCode">A code the KeyboardHook procedure uses to determine how to process the message.</param>
		/// <param name="wParam">The key state.</param>
		/// <param name="lParam">The key pressed.</param>
		/// <returns>A value indicating whether or not to process additional hooks in the current hook chain.</returns>
		private IntPtr HookCallBack(int nCode, IntPtr wParam, IntPtr lParam)
		{
			int keyState = (int)wParam;
			bool isKeyUp = keyState == NativeMethods.Constants.WM_KEYUP || (keyState == NativeMethods.Constants.WM_SYSKEYUP);
			bool isKeyDown = keyState == NativeMethods.Constants.WM_KEYDOWN || (keyState == NativeMethods.Constants.WM_SYSKEYDOWN);

			if (isKeyUp)
			{
				this.wasHookKeyReleased = true;
			}

			// If AutoRepeat is on, always process hook; otherwise, only process hook if they key
			// has been released and re-pressed.
			bool allowHook = this.AutoRepeat ? true : this.wasHookKeyReleased;

			// If we don't process the hook because it's a repeated key press, then unless AllowPassThrough
			// is on, method should return and block further processing. (If AllowPassThrough is true, 
			// CallNextHook should be called next.)
			if (!allowHook && !this.AllowPassThrough)
			{
				return new IntPtr(1);
			}

			if (allowHook)
			{
				// MSDN documentation indicates that nCodes less than 0 should always only invoke CallNextHookEx.
				if (nCode >= 0 && isKeyDown)
				{
					EnhancedKeys unmodifiedKeyPressed = (EnhancedKeys)Marshal.ReadInt32(lParam);
					if (unmodifiedKeyPressed == this.UnmodifiedKey && this.AreModifierKeysPressed())
					{
						this.wasHookKeyReleased = false;
						this.OnPressed(this, new EventArgs());

						if (!this.AllowPassThrough)
						{
							// A non-zero return value blocks additional processing of key strokes.
							return new IntPtr(1);
						}
					}
				}
			}

			return NativeMethods.CallNextHookEx(this.hookID, nCode, wParam, lParam);
		}
	}
}
